package com.spzx.product.aops;

import com.spzx.common.core.utils.uuid.UUID;
import com.spzx.product.annotation.MyCacheAnnotation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.TimeUnit;

@Component
@Slf4j
@Aspect
public class MyRedisCacheAspect {
    @Autowired
    private RedisTemplate redisTemplate;

    @Pointcut("@annotation(com.spzx.product.annotation.MyCacheAnnotation)")
    public void pc() {
    }

    ;


    /**
     * 环绕通知方法，用于实现基于注解的缓存逻辑。
     * 该方法在目标方法执行前后进行拦截，首先尝试从Redis缓存中获取数据，
     * 如果缓存未命中，则获取分布式锁，执行目标方法并将结果写入缓存。
     *
     * @param proceedingJoinPoint 连接点对象，用于获取目标方法的信息和执行目标方法
     * @return 目标方法的执行结果或缓存中的数据
     * @throws Throwable 可能抛出的异常
     */
    @Around("pc()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 声明结果变量，初始化为null，用于存储最终返回的结果
        Object result = null;

        // 获取目标方法的签名对象，通过该对象可以获取方法的各种元信息
        // 使用MethodSignature可以获取方法的详细信息，包括参数、返回值等
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();

        // 通过签名对象获取具体的方法对象，用于后续获取方法上的注解信息
        // getMethod()方法返回java.lang.reflect.Method对象，包含方法的所有反射信息
        Method method = signature.getMethod();

        // 获取方法上定义的MyCacheAnnotation注解对象
        // getAnnotation()方法用于获取指定类型的注解实例
        MyCacheAnnotation annotation = method.getAnnotation(MyCacheAnnotation.class);

        // 从注解中获取缓存key的前缀部分，用于构建完整的缓存key
        // keyPrefix()方法返回注解中定义的keyPrefix属性值
        String keyPrefix = annotation.keyPrefix();

        // 从注解中获取用于构建缓存key的SpEL表达式字符串
        // matchValue()方法返回注解中定义的matchValue属性值
        String matchValue = annotation.matchValue();

        // 创建SpEL表达式解析器实例，用于解析Spring表达式语言
        // SpelExpressionParser是Spring提供的表达式解析器，用于解析和计算SpEL表达式
        SpelExpressionParser spelExpressionParser = new SpelExpressionParser();

        // 使用解析器将matchValue字符串解析为Expression表达式对象
        // parseExpression()方法将字符串表达式解析为可执行的Expression对象
        Expression expression = spelExpressionParser.parseExpression(matchValue);

        // 创建标准的SpEL评估上下文，用于存储表达式中使用的变量
        // StandardEvaluationContext提供了标准的上下文实现，支持属性访问、方法调用等
        EvaluationContext context = new StandardEvaluationContext();

        // 获取目标方法调用时传入的实际参数值数组
        // getArgs()方法返回连接点方法调用时传递的所有参数值
        Object[] args = proceedingJoinPoint.getArgs();

        // 创建参数名发现器实例，用于获取方法参数的名称
        // DefaultParameterNameDiscoverer是Spring提供的参数名发现器实现
        DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();

        // 通过参数名发现器获取方法参数的名称数组
        // getParameterNames()方法根据方法对象获取参数名称列表
        String[] parameterNames = discoverer.getParameterNames(method);

        // 遍历所有参数，将参数名称和对应的值设置到SpEL上下文中
        // 这样在表达式中就可以通过参数名称引用实际参数值
        for (int i = 0; i < parameterNames.length; i++) {
            // 打印参数名和参数值到控制台，用于调试目的
            System.out.println(parameterNames[i] + ":" + args[i]);

            // 将参数名和参数值作为变量设置到SpEL上下文中
            // setVariable()方法在上下文中设置命名变量，供表达式使用
            context.setVariable(parameterNames[i], args[i]);
        }

        try {
            // 使用SpEL表达式和上下文计算出完整的缓存key
            // getValue()方法计算表达式的值，结合上下文中的变量
            String key = keyPrefix + ":" + expression.getValue(context);

            // 打印将要查询的Redis key到控制台，用于调试和监控
            System.out.println("------redis 查询 key: " + key);

            // 从Redis中获取指定key对应的缓存值
            // opsForValue()返回操作字符串类型数据的ValueOperations对象
            // get()方法根据key获取Redis中存储的值
            result = redisTemplate.opsForValue().get(key);
            log.info("查询缓存，线程ID：{}，线程名称：{}", Thread.currentThread().getId(), Thread.currentThread().getName());
            // 检查Redis中是否存在指定的key
            // hasKey()方法检查给定key是否存在于Redis中
            if (redisTemplate.hasKey(key)) {
                // 如果key存在，说明缓存命中，记录日志并直接返回缓存结果
                // info()方法记录INFO级别的日志信息
                // getId()和getName()获取当前线程的ID和名称，用于追踪并发情况
                log.info("命中缓存，直接返回，线程ID：{}，线程名称：{}", Thread.currentThread().getId(), Thread.currentThread().getName());
                return result;
            }

            // 缓存未命中，构造分布式锁的key，用于防止缓存击穿
            String lockkey = keyPrefix + ":lock:" + expression.getValue(context);

            // 生成一个唯一的锁值，用于安全地释放锁
            // UUID.randomUUID()生成唯一的标识符，replace()方法去除其中的横线
            String lockValue = UUID.randomUUID().toString().replace("-", "");

            // 尝试获取分布式锁，使用SETNX命令（setIfAbsent），并设置10秒过期时间防止死锁
            // setIfAbsent()方法实现Redis的SETNX命令，只有key不存在时才设置
            // 设置过期时间是为了防止获取锁后程序异常退出导致的死锁问题
            Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent(lockkey, lockValue, 10, TimeUnit.SECONDS);

            // 判断是否成功获取到分布式锁
            if (ifAbsent) {
                // 获取锁成功，记录日志信息
                log.info("获取锁成功：{}，线程名称：{}", Thread.currentThread().getId(), Thread.currentThread().getName());

                try {
                    // 执行目标方法，获取实际的业务结果
                    // proceed()方法执行连接点处的方法，即被拦截的实际方法
                    result = proceedingJoinPoint.proceed();

                    // 计算缓存的过期时间：空值缓存60秒，非空值缓存600秒加0-19的随机数
                    // 这样可以防止大量缓存在同一时间过期导致的缓存雪崩
                    long ttl = (result == null ? 60 : 600) + new Random().nextInt(20);

                    // 将方法执行结果存入Redis缓存，并设置过期时间
                    // set()方法将key-value对存储到Redis中，并设置过期时间
                    redisTemplate.opsForValue().set(key, result, ttl, TimeUnit.SECONDS);
                    log.info("设置缓存，线程ID：{}，线程名称：{}", Thread.currentThread().getId(), Thread.currentThread().getName());
                    // 返回执行结果
                    return result;
                } finally {
                    // 无论是否发生异常，都需要释放分布式锁

                    // 定义Lua脚本，用于原子性地判断并删除锁
                    // 使用Lua脚本可以保证判断和删除操作的原子性，防止误删其他线程的锁
                    String scriptText = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                            "then\n" +
                            "    return redis.call(\"del\",KEYS[1])\n" +
                            "else\n" +
                            "    return 0\n" +
                            "end";

                    // 创建Redis脚本对象，用于执行Lua脚本
                    // DefaultRedisScript是Spring Data Redis提供的脚本执行实现
                    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();

                    // 设置Lua脚本的内容
                    // setScriptText()方法设置要执行的Lua脚本代码
                    redisScript.setScriptText(scriptText);

                    // 设置脚本的返回类型，这里返回Long类型
                    // setResultType()方法指定Lua脚本执行后的返回值类型
                    redisScript.setResultType(Long.class);

                    // 执行Lua脚本释放锁，确保只有持有锁的线程才能释放锁
                    // execute()方法执行Lua脚本，传入key列表和参数列表
                    redisTemplate.execute(redisScript, Arrays.asList(lockkey), lockValue);
                }
            } else {
                // 获取锁失败，可能是其他线程正在执行相同的方法

                // 等待200毫秒，避免频繁重试造成系统压力
                // sleep()方法使当前线程暂停指定毫秒数
                Thread.sleep(200);

                // 记录获取锁失败的日志信息
                log.error("获取锁失败，自旋：{}，线程名称：{}", Thread.currentThread().getId(), Thread.currentThread().getName());

                // 重新执行当前方法（会再次进入切面逻辑）
                // 通过递归调用实现自旋等待，直到获取到锁或缓存被其他线程填充
                return this.around(proceedingJoinPoint);
            }
        } catch (Throwable e) {
            // 捕获所有可能的异常，包括方法执行异常和Redis操作异常
            // printStackTrace()方法打印异常堆栈信息到标准错误输出
            e.printStackTrace();

            // 发生异常时，直接执行目标方法并返回结果，跳过缓存逻辑
            // proceed()方法在异常情况下直接执行原方法，保证业务流程不中断
            return proceedingJoinPoint.proceed();
        }
    }

}
